package main

import (
	"crypto/md5"
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"github.com/patrickmn/go-cache"
	"html/template"
	"io/ioutil"
	"log"
	"math/rand"
	. "net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"time"
)

type proxy struct {
	server  *Server
	handler *targetHandler
}

type targetHandler struct {
	oAuthApi     string
	oAuthUrl     string
	targetUrl    *url.URL
	reverseProxy *httputil.ReverseProxy
}

func NewProxy(listen, targetUrl string) (*proxy, error) {
	u, er := url.Parse(targetUrl)
	if er != nil {
		return nil, er
	}

	h := &targetHandler{targetUrl: u}
	h.reverseProxy = httputil.NewSingleHostReverseProxy(h.targetUrl)

	s := &Server{Addr: listen, Handler: h}

	p := &proxy{
		handler: h,
		server:  s,
	}
	log.Println("create proxy to", targetUrl, " by http://[address]"+listen)

	return p, nil
}

func (p *proxy) start() error {
	return p.server.ListenAndServe()
}

var sessionStore *cache.Cache

const sessionId = "GoSessionId"

const (
	loginPath      = "/login"
	logoutPath     = "/logout"
	redirectUriKey = "redirect_uri"
)

var temp *template.Template

func init() {
	sessionStore = cache.New(time.Hour*12, time.Hour*24)
}

func init() {
	t := `
{{ define "login" }}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/css/bootstrap.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet">
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    <script src="https://cdn.bootcss.com/twitter-bootstrap/4.3.1/js/bootstrap.js"></script>
    <title>登录</title>
</head>
<body>
<div>
    <div class="be-content pren">
        <div class="ioc_text">
            <img src="" alt="">
            <span>请登录您的用户</span>
        </div>
        <div>
            <form action="" method="post">
                <div class="br-content">
                    <div class="input-group mb-4 bootint">
                        <div class="input-group-prepend">
                            <span class="input-group-text"><i class="fa fa-user"></i></span>
                        </div>
                        <input type="text" name="username" class="form-control" placeholder="用户名">
                    </div>
                    <div class="input-group mb-4 bootint">
                        <div class="input-group-prepend">
                            <span class="input-group-text"><i class="fa fa-unlock-alt"></i></span>
                        </div>
                        <input type="password" name="password" class="form-control" placeholder="密码">
                    </div>
                    <div class="br-text">
                    </div>
                    <div style="padding-top: 10px">
                        <input type="submit" class="btn" value="登录">
                    </div>
                    <div class="be-con">
                        <span>Copyright © 2019 - ~ <a href="#">ACL代理工具</a></span>
                    </div>
                </div>
            </form>
        </div>
    </div>
</div>
<style>
    body {
        background-color: #eeeeee;
    }
    .be-content {
        max-width: 410px;
        margin: 100px auto;
        background-color: #ffffff;
        box-shadow: -2px 2px 4px 0px rgba(0, 0, 0, 0.04);
        border-width: 0;
        border-top-width: 0px;
        border-radius: 3px;
    }
    .br-content {
        width: 301px;
        margin: 0px auto;
    }
    .bootint {
        width: 300px
    }
    .ioc_text {
        text-align: center;
        padding-top: 30px;
        margin-bottom: 50px;
    }
    .ioc_text span {
        display: block;
        font-size: 13px;
        margin-top: 11px;
    }
    .ioc_text img {
        width: 106px;
        height: 28px;
    }
    .br-text {
        font-size: 12px;
        padding-top: 10px;
    }
    .btn {
        width: 300px;
        height: 38px;
        color: white;
        background-color: #1266f1;
    }
    .be-con {
        width: 190px;
        height: 14px;
        font-size: 11px;
        margin: 50px auto;
        padding-bottom: 50px;
    }
</style>
</body>
</html>
{{ end }}
`
	var er error
	tp := template.New("login")
	temp, er = tp.Parse(t)
	if er != nil {
		panic(er)
	}
}

func randStr() string {
	now := strconv.Itoa(int(time.Now().UnixNano()))
	rd := strconv.Itoa(rand.Int())

	s := now + rd
	h := md5.Sum([]byte(s))

	d := base64.StdEncoding.EncodeToString(h[:])

	return d
}

func redirect(w ResponseWriter, r *Request, target string) {
	w.Header().Set("Location", target)
	w.Write([]byte("<script>window.location='" + target + "'</script>"))
}

func redirectBy(w ResponseWriter, r *Request) {
	redirectUri := r.FormValue(redirectUriKey)
	if redirectUri == "" {
		redirectUri = "/"
	}
	redirect(w, r, redirectUri)
}

func backUrl(r *Request) string {
	v := url.Values{redirectUriKey: []string{r.URL.String()}}
	return "?" + v.Encode()
}

func login(w ResponseWriter, r *Request) {

	switch r.Method {
	case MethodGet:
		temp.ExecuteTemplate(w, "login", []interface{}{})
	case MethodPost:
		username := strings.TrimSpace(r.PostFormValue("username"))
		password := strings.TrimSpace(r.PostFormValue("password"))
		if username == globalConfig.Username && password == globalConfig.Password {
			// 登录成功
			expire := globalConfig.SessionExpire
			sid := randStr()
			m := make(map[string]interface{})
			m["username"] = username
			sessionStore.Set(sid, m, time.Second*time.Duration(expire))
			uc := &Cookie{
				Name:     sessionId,
				Value:    sid,
				Path:     "/",
				HttpOnly: false,
				MaxAge:   expire,
			}
			SetCookie(w, uc)

			// 跳转指定页面
			log.Println("debug: this url is ", r.URL.String())
			redirectUri := r.URL.Query().Get(redirectUriKey)
			log.Println("Login success, immediately redirect to: ", redirectUri)
			if redirectUri == "" {
				redirectUri = "/"
			}
			redirectBy(w, r)
		} else {
			redirect(w, r, r.URL.String())
		}
	default:
		w.WriteHeader(StatusMethodNotAllowed)
		w.Write([]byte("404 Not Found"))
	}
}

func logout(w ResponseWriter, r *Request) {
	uc, er := r.Cookie(sessionId)
	if er == nil && nil != uc {
		sid := uc.Value
		sessionStore.Delete(sid)
		uc.MaxAge = -1
		SetCookie(w, uc)
	}
}

func auth(w ResponseWriter, r *Request) bool {
	uc, er := r.Cookie(sessionId)
	if er == nil && nil != uc {
		sid := uc.Value
		session, exist := sessionStore.Get(sid)
		if exist && session != nil {
			expire := globalConfig.SessionExpire
			sessionStore.Set(sid, session, time.Second*time.Duration(expire))
			return true
		}
	}
	log.Printf("Redirect to Login")

	redirectUri := loginPath + "?" + url.Values{redirectUriKey: []string{r.URL.String()}}.Encode()
	log.Println("After login, redirect to this uri: ", redirectUri)
	redirect(w, r, redirectUri)
	return false
}

func (h *targetHandler) ServeHTTP(w ResponseWriter, r *Request) {
	// 检查登录页面
	if strings.HasPrefix(r.URL.Path, loginPath) {
		login(w, r)
		return
	} else if strings.HasPrefix(r.URL.Path, logoutPath) {
		logout(w, r)
		return
	}

	if !auth(w, r) {
		return
	}

	h.reverseProxy.ServeHTTP(w, r)
}

type Config struct {
	SessionExpire int `json:"session_expire"`

	Username string `json:"username"`
	Password string `json:"password"`

	Resources []string `json:"resources"`

	Proxies []struct {
		Listen string `json:"listen"`
		Target string `json:"target"`
	} `json:"proxies"`
}

var globalConfig Config

func main() {
	confFile := flag.String("config", "config.json", "json file")
	flag.Parse()

	if nil != confFile {
		confData, er := ioutil.ReadFile(*confFile)
		er = json.Unmarshal(confData, &globalConfig)
		if er != nil {
			panic(er)
		}
		pretty, _ := json.MarshalIndent(globalConfig, "", "\t")
		fmt.Println(string(pretty))
	}
	if 0 == len(globalConfig.Proxies) {
		log.Println("no proxy configurations")
		return
	}

	var wg sync.WaitGroup

	for _, p := range globalConfig.Proxies {
		p, er := NewProxy(p.Listen, p.Target)
		if er != nil {
			log.Fatal(er)
		}
		wg.Add(1)
		go func() {
			defer wg.Done()
			er = p.start()
			if er != nil {
				log.Fatal(er)
			}
		}()
	}

	wg.Wait()
}
